package business_logic;

import java.io.FileWriter;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import controller.gameloop.GameLoop;
import controller.gameloop.GameLoopCommand;

import fiuba.algo3.titiritero.modelo.SuperficieDeDibujo;

import model.elements.Element2D;
import model.elements.NonPlayerCharacter;
import model.elements.attributes.Coordinate2D;
import model.elements.commands.ElementCommand;
import model.elements.commands.ElementCommandFactory;
import model.elements.states.movement.MovingDown;
import model.elements.states.movement.MovingLeft;
import model.elements.states.movement.MovingRight;
import model.elements.states.movement.MovingUp;
import model.elements.states.movement.Stopped;
import model.exceptions.CommandFactoryIsNotReady;
import model.exceptions.ModelExceptions;
import model.map.ElementsMap;
import business_logic.database.GameDatabase;
import business_logic.game_elements.BloqueAcero;
import business_logic.game_elements.BloqueCemento;
import business_logic.game_elements.BloqueLadrillos;
import business_logic.game_elements.Bombita;
import business_logic.game_elements.Cecilio;
import business_logic.game_elements.Dropper;
import business_logic.game_elements.LopezReggae;
import business_logic.game_elements.LopezReggaeAlado;
import business_logic.game_elements.Portal;
import business_logic.view.GameView;
import business_logic.view.GameViewIdentifier;
import business_logic.view.HighScoresView;
import business_logic.view.PauseMessage;
import business_logic.view.FadeOutText;
import business_logic.view.ScoreText;
import business_logic.view.BonusScoreView;
import business_logic.view.YouLoseText;
import business_logic.view.YouWinText;

public class Game
{
    
    /**
     * Constantes
     */
    private static final int DEFAULT_TILES_X = 30;
    private static final int DEFAULT_TILES_Y = 20;
    private static final long GAME_VERSION = 2;
    private static final String HIGHSCORES_FILENAME = new String("highscores.xml");
    
    /**
     * Configuración de tamaño
     */
    private final int lastXTileIndex;
    private final int lastYTileIndex;
    
    /**
     * Objetos necesarios para hacer funcionar al juego
     */
    private final GameLoop m_gameLoop;
    private final GameDatabase m_database = new GameDatabase();
    private final GameViewFactory m_viewFactory = new GameViewFactory();
    private final ElementsMap m_map;
    
    /**
     * Contenedores por Categoria de elementos
     */
    private Bombita m_player = null;
    private Portal  m_portal = null;
    private Collection<NonPlayerCharacter> m_enemies  = new HashSet<NonPlayerCharacter>();
    private List<GameElement>              m_drops    = new LinkedList<GameElement>();
    private LinkedList<GameElement>        m_droppers = new LinkedList<GameElement>();
    
    private LinkedList<GameLevel> m_levels;
    private int m_currentScore = 0;
    private int m_timeLeft = 0;
    private int m_winBonus = 0;
    
    private PauseMessage               m_pauseMessage = new PauseMessage();
    private BonusScoreView               m_timeLeftView = new BonusScoreView( m_timeLeft );
    private ScoreText                  m_scoreMessage = new ScoreText( 0 );
    private GameLevel                  m_currentLevel = null;
    private boolean                    m_forcedWinLevel = false;    // Cheat para pasar de nivel directamente
    
    private HighScores                 m_highScores = new HighScores(10);
    private HighScoresView             m_highScoresBoard = new HighScoresView(m_highScores);
    
    /**
     * Constructores e inicializadores
     */
    
    public Game( int tilesX, int tilesY, SuperficieDeDibujo mainPanel )
    {
        lastXTileIndex = tilesX-1;
        lastYTileIndex = tilesY-1;
        m_gameLoop = new GameLoop((SuperficieDeDibujo) mainPanel);
        m_map = new ElementsMap(m_database, lastXTileIndex, lastYTileIndex);

        this.init();
    }
    
    public Game( SuperficieDeDibujo mainPanel )
    {
        lastXTileIndex = DEFAULT_TILES_X-1;
        lastYTileIndex = DEFAULT_TILES_Y-1;
        m_gameLoop = new GameLoop((SuperficieDeDibujo) mainPanel);
        m_map = new ElementsMap(m_database, lastXTileIndex, lastYTileIndex);

        this.init();
    }

    private void init()
    {
        // TODO: OJO CON ESTO, la fábrica no debería ser static.... o esto no debería ir aca...
        ElementCommandFactory.setup( m_map, new GameElementsFactory() );
        loadGameLoopListeners();

        m_levels = new LinkedList<GameLevel>();
        
        m_levels.add( new GameLevel("level1.xml") );
        m_levels.add( new GameLevel("level2.xml") );
        m_levels.add( new GameLevel("level3.xml") );
        m_levels.add( new GameLevel("level4.xml") );
        m_levels.add( new GameLevel("level5.xml") );
        
        m_highScores.load(HIGHSCORES_FILENAME);
    }
    
    public void showScoreBoard()
    {
        m_gameLoop.add(m_highScoresBoard);
        m_highScoresBoard.setEnabled(true);
    }
    
    private void loadGameLoopListeners()
    {
        this.m_gameLoop.addBeforeRunListener ( new GameLoopCommand()
        {
            @Override
            public void execute(GameLoop gameloop)
            {
                Collection<GameElement> uninitializedGameElements = m_database.getElementsWithoutView();
                
                for ( GameElement gameElement : uninitializedGameElements )
                {
                    // Inicializo el objeto para poder ser usado por el controlador
                        
                    // Obtengo las vistas asociadas al elemento
                    Collection<GameViewIdentifier> viewIdentifiers = gameElement.getViewIdentifiers();
                    Collection<GameView> views = new HashSet<GameView>();
                    
                    for ( GameViewIdentifier viewIdentifier : viewIdentifiers )
                    {
                        views.add( m_viewFactory.createViewForGameElement(viewIdentifier, gameElement) );
                    }
                    
                    // Registro la vista ( o las vistas ) en el gameLoop y la DB
                    for ( GameView view : views )
                    {
                        m_gameLoop.add( view );
                        try
                        {
                            m_database.addViewForElement( view, gameElement );
                        }
                        catch (Exception e)
                        {
                            e.printStackTrace();
                        }
                    }
                    
                    // Registro el elemento en el gameLoop
                    m_gameLoop.add( gameElement );
                }
            }
        });
        
        // TODO: Mejorar esto!
        // Check game state (won, lost) and act accordingly
        this.m_gameLoop.addAfterRunListener ( new GameLoopCommand()
        {    
            @Override
            public void execute(GameLoop gameloop)
            {       
                if ( gameloop.hasDraw() )
                {
                    if ( playerIsDead() ) 
                        gameLost();                      
                    else
                        if( ( noMoreEnemiesAlive() && portalHasBeenCrossed() ) || m_forcedWinLevel)
                            levelWon();
                }                 
            }
        });
        
        // Remove unused elements
        this.m_gameLoop.addAfterRunListener ( new GameLoopCommand()
        {
            @Override
            public void execute(GameLoop gameloop)
            {
                Collection<GameElement> uselessElements = m_database.getUselessElements();
                
                for ( GameElement uselessElement : uselessElements )
                {
                    
                    if ( uselessElement != m_player )
                    {
                        
                        Collection<GameView> uselessViews = m_database.getViewsFor( uselessElement );
                        
                        for ( GameView uselessView : uselessViews )
                            // Remuevo las vistas del elemento del gameLoop
                            m_gameLoop.remove( uselessView );
                        
                        // Remuevo el elemento del game Loop
                        m_gameLoop.remove( uselessElement );
                        
                        // Remuevo el elemento (y sus vistas) de la DB
                        m_database.removeElement( uselessElement );   
                    }
                }
                    
            }
        });
        
        // Update Level Time left
        this.m_gameLoop.addAfterRunListener ( new GameLoopCommand()
        {
            private int m_ticksCounter = 0;

            @Override
            public void execute(GameLoop gameloop)
            {
                if ( m_ticksCounter % 250 == 0 )
                    if( m_timeLeft > 0 )
                    {
                        m_timeLeft--;
                        m_timeLeftView.updateTime(m_timeLeft);                        
                    }
                m_ticksCounter++;
            }
        });

    }
    
    /**
     * Métodos básicos para controlar el estado del juego: start, stop, isRunning, etc...
     */
    
    private void clear()
    {
        m_database.clear();
        m_gameLoop.clear();
        m_droppers.clear();
        m_drops.clear();
        m_enemies.clear();
    }
    
    // TODO: completar esta condicion
    private boolean canPause()
    {
        return m_gameLoop.isRunning() && m_currentLevel!=null;
    }
    
    // TODO: completar esta condicion
    private boolean canResume()
    {
        return (!m_gameLoop.isRunning());
    }
    
    public boolean pause()
    {
        if( canPause() )
        {
            m_pauseMessage.setEnabled(true);
            this.stop();
            return true;
        }
        return false;
    }
    
    public boolean resume()
    {
        
        System.out.println( "GAME: RESUME" );
        
        if( canResume() )
        {
            m_pauseMessage.setEnabled(false);
            this.start();
            return true;
        }
        return false;
    }
    
    private void stop()
    {
        if( m_gameLoop.isRunning() )
        {
            m_gameLoop.pause();
        }
    }
    
    private void start()
    {
        System.out.println( "GAME: START" );
        if(!m_gameLoop.isRunning())
            m_gameLoop.start();
    }
    
    public boolean isRunning()
    {
        return m_gameLoop.isRunning() && m_currentLevel!=null;
    }
    
    public void newGame()
    {
        m_currentLevel = m_levels.getFirst();
        m_currentScore = 0;
        this.loadPreMadeLevel(m_currentLevel);
    }
    
    /**
     *  Métodos para realizar acciones sobre el player
     */
    
    private boolean dennyPlayerCommands()
    {
        return !isRunning() || m_currentLevel==null;
    }
    
    public void playerStops()
    {
        if ( dennyPlayerCommands() )
            return;
        
        try
        {
            ElementCommand command;
            command = ElementCommandFactory.getElementMove( new Stopped() );
            m_player.addCommand(command);
        }
        catch (CommandFactoryIsNotReady e)
        {
            e.printStackTrace();
        }
    }
    
    public void playerMovesRight()
    {
        if ( dennyPlayerCommands() )
            return;
        
        try
        {
            ElementCommand command;
            command = ElementCommandFactory.getElementMove( new MovingRight() );
            m_player.addCommand(command);
        }
        catch (CommandFactoryIsNotReady e)
        {
            e.printStackTrace();
        }
    }
    
    public void playerMovesLeft()
    {
        if ( dennyPlayerCommands() )
            return;
        
        try
        {
            ElementCommand command;
            command = ElementCommandFactory.getElementMove( new MovingLeft() );
            m_player.addCommand(command);
        }
        catch (CommandFactoryIsNotReady e)
        {
            e.printStackTrace();
        }
    }
    
    public void playerMovesUp()
    {
        if ( dennyPlayerCommands() )
            return;
        
        try
        {
            ElementCommand command;
            command = ElementCommandFactory.getElementMove( new MovingDown() );
            m_player.addCommand(command);
        }
        catch (CommandFactoryIsNotReady e)
        {
            e.printStackTrace();
        }
    }

    public void playerMovesDown()
    {
        if ( dennyPlayerCommands() )
            return;
        
        try
        {
            ElementCommand command;
            command = ElementCommandFactory.getElementMove( new MovingUp() );
            m_player.addCommand(command);
        }
        catch (CommandFactoryIsNotReady e)
        {
            e.printStackTrace();
        }
    }
    
    public void playerDropsBomb()
    {
        if ( dennyPlayerCommands() )
            return;
        
        try
        {
            ElementCommand command;
            command = ElementCommandFactory.getElementUseWeapon();
            m_player.addCommand(command);
        }
        catch (CommandFactoryIsNotReady e)
        {
            e.printStackTrace();
        }
    }
    
    /**
     * Métodos para persistir el estado del juego (save y load)
     */
    
    private void beforeLoadLevel()
    {
        
        this.clear();

        /**
         * Agrego la vista de pausa
         */
        m_pauseMessage.setEnabled(false);
        this.m_gameLoop.add( m_pauseMessage );
        
        
        /**
         * Agrego la vista del current level
         */
        String levelName = new String("Nivel: "+(m_levels.indexOf(m_currentLevel)+1));
        GameView NewLevelLoadedNotification = new FadeOutText( levelName , 60 );
        this.m_gameLoop.add( NewLevelLoadedNotification );
        
        /**
         * Agrego la vista del current Score
         */
        m_scoreMessage.updateScore(m_currentScore);
        this.m_gameLoop.add( m_scoreMessage );
        
        /**
         * Agrego la vista del Time Left
         */
        this.m_gameLoop.add( m_timeLeftView );

    }
    
    private void afterLoadLevel()
    {
        
        /**
         * Instancio portal!
         */
        
        Portal portal;
        try
        {
            portal = new Portal( new Coordinate2D() );
            this.registerPortal(portal);
        }
        catch (ModelExceptions e)
        {
            e.printStackTrace();
        }
        
        /**
         * Asigno drops
         */
        this.randomlyAssignDrops();
        
        /**
         * Actualizo el time Left
         */
        m_timeLeftView.updateTime(m_timeLeft);
        
    }
    
    public void saveCustomGame( String filename )
    {
        Document doc = DocumentHelper.createDocument();
        Element root = DocumentHelper.createElement("CustomSavedGame");
        
        Element infoNode = DocumentHelper.createElement("Info");
        infoNode.addAttribute( "version", Game.GAME_VERSION+"" );
        root.add(infoNode);
        
        // Esto guarda:
        // El current score
        // El current lvl
        // El estado de todos los elementos del juego.
        Element dataNode = DocumentHelper.createElement("Data");
        dataNode.addAttribute( "level", m_levels.indexOf(m_currentLevel)+"" );
        dataNode.addAttribute( "score", m_currentScore+"" );
        root.add(dataNode);
        
        root.add( this.m_database.save() );
        
        Element timeNode = DocumentHelper.createElement("Time");
        timeNode.addAttribute( "left", m_timeLeft+"" );
        timeNode.addAttribute( "bonus", m_winBonus+"" );
        root.add(timeNode);
        
        doc.add(root);
        
        try
        {
            FileWriter writer;
            writer = new FileWriter(filename);
            doc.write( writer);
            writer.close();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
    
    public void loadCustomGame( String filename )
    {
        try
        {
            this.stop();
            
            // Le vamos a dar tiempo a que frene el Thread del gameLoop, se q con un sleep no debería hacerse,
            //  pero como es un problema de concurrencia, dudo que este mal hacerlo asi aca.
            Thread.sleep(100);
            
            this.clear();

            /**
             * Agrego la vista de pausa
             */
            m_pauseMessage.setEnabled(false);
            this.m_gameLoop.add( m_pauseMessage );
            
            /**
             * Agrego la vista del current Score
             */
            m_scoreMessage.updateScore(m_currentScore);
            this.m_gameLoop.add( m_scoreMessage );
            
            /**
             * Agrego la vista del Time Left
             */
            this.m_gameLoop.add( m_timeLeftView );
            
            SAXReader reader = new SAXReader();

            System.out.println( "Loading Custom Game: "+filename );

            Document document;
            document = reader.read( filename );
            Element root = document.getRootElement();
            
            // Cargo el Nº de Level
            Element data = root.element("Data");
            int levelIndex = Integer.parseInt(data.attributeValue("level"));
            m_currentLevel = m_levels.get(levelIndex);
            
            // Cargo el puntaje del jugador            
            m_currentScore = Integer.parseInt(data.attributeValue("score"));
            
            // Cargo el level
            this.restore( root.element("Game_Snapshot") );
            
            Element timeData = root.element("Time");
            m_timeLeft = Integer.parseInt(timeData.attributeValue("left"));
            m_winBonus = Integer.parseInt(timeData.attributeValue("bonus"));
            
            /**
             * Agrego la vista del current level
             */
            String levelName = new String("Load: "+(m_levels.indexOf(m_currentLevel)+1));
            GameView NewLevelLoadedNotification = new FadeOutText( levelName , 60 );
            this.m_gameLoop.add( NewLevelLoadedNotification );
            
            m_timeLeftView.updateTime(m_timeLeft);
            
            this.start();
            
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
    
    private enum GameElementType
    {
        PLAYER,
        NPC,
        WALL_DROPPER,
        WALL_NON_DROPPER,
        PORTAL,
        OTHER
    }
    
    private GameElementType parseType( String s )
    {
        if ( s.equalsIgnoreCase( "business_logic.game_elements.Bombita" ) )
            return GameElementType.PLAYER;
        
        if ( s.equalsIgnoreCase( "business_logic.game_elements.Cecilio" ) || s.equalsIgnoreCase( "business_logic.game_elements.LopezReggae" ) || s.equalsIgnoreCase( "business_logic.game_elements.LopezReggaeAlado" ) )
            return GameElementType.NPC;
        
        if ( s.equalsIgnoreCase( "business_logic.game_elements.BloqueLadrillos" ) || s.equalsIgnoreCase( "business_logic.game_elements.BloqueCemento" ) )
            return GameElementType.WALL_DROPPER;
        
        if ( s.equalsIgnoreCase( "business_logic.game_elements.BloqueAcero" ) )
            return GameElementType.WALL_NON_DROPPER;
        
        if ( s.equalsIgnoreCase( "business_logic.game_elements.Portal" ) )
            return GameElementType.PORTAL;
        
        return GameElementType.OTHER;
    }
    
    private void restore( Element root )
    {
        try
        {
            @SuppressWarnings("unchecked")
            Iterator<Element> it = root.elementIterator();
            
            while ( it.hasNext() )
            {
                // Instanciar el elemento
                Element elem = it.next();
                
                //System.out.println( "Procesando: "+elem.getName() );
                
                GameElement newInstance = (GameElement) Class.forName( elem.getName() ).getDeclaredConstructor( Element.class ).newInstance( elem );
                
                // registrarlo en el juego
                switch( parseType ( elem.getName() ) )
                {
                    case PLAYER :
                        this.registerPlayerCharacter( (Element2D) newInstance );
                        break;

                    case NPC :
                        this.registerNonPlayerCharacter( (Element2D) newInstance );
                        break;
                    
                    case WALL_DROPPER :
                        this.registerWall( (Element2D) newInstance , true);
                        
                        // Checkear aca si tiene como drop el portal
                        GameElement drop = ( (Dropper)newInstance ).getDrop();
                        
                        if ( drop!=null && drop.getClass() == Portal.class )
                        {
                            m_portal = (Portal) ( (Dropper)newInstance ).getDrop();
                            this.addBasicElement( (Element2D) newInstance );
                        }
                            
                        break;
                    
                    case WALL_NON_DROPPER :
                        this.registerWall( (Element2D) newInstance , false);
                        break;
                    
                    case PORTAL:
                        m_portal = (Portal) newInstance;
                        this.addBasicElement( (Element2D) newInstance );
                        break;
                        
                    default:
                        this.addBasicElement( (Element2D) newInstance );
                }
                
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        
    }
    
    // TODO: SACAR!
    public void forceWinLevel()
    {
        m_forcedWinLevel = true;
    }
    
    private void loadPreMadeLevel(GameLevel lvl)
    {
        try
        {
            this.beforeLoadLevel();
            
            SAXReader reader = new SAXReader();
            
            System.out.println( "Loading Level: "+lvl.getFilename() );
            
            Document document = reader.read( lvl.getFilename() );
            Element root = document.getRootElement();
            
            Element playerData = root.element("Player");
            double playerX = Double.parseDouble(playerData.attributeValue("x"));
            double playerY = Double.parseDouble(playerData.attributeValue("y"));
            Bombita player = new Bombita( new Coordinate2D(playerX,playerY) );
            this.registerPlayerCharacter(player);
            
            Element npcData = root.element("NPC");
            @SuppressWarnings("unchecked")
            Iterator<Element> cecilios = npcData.elementIterator("business_logic.game_elements.Cecilio");
            while( cecilios.hasNext() )
            {
                Element elem = cecilios.next();
                
                double x = Double.parseDouble(elem.attributeValue("x"));
                double y = Double.parseDouble(elem.attributeValue("y"));
                
                Cecilio cecilio = new Cecilio( new Coordinate2D( x,y ) );
                registerNonPlayerCharacter(cecilio);
            }
            
            @SuppressWarnings("unchecked")
            Iterator<Element> lopezReggaes = npcData.elementIterator("business_logic.game_elements.LopezReggae");
            while( lopezReggaes.hasNext() )
            {
                Element elem = lopezReggaes.next();
                
                double x = Double.parseDouble(elem.attributeValue("x"));
                double y = Double.parseDouble(elem.attributeValue("y"));
                
                LopezReggae lopezReggae = new LopezReggae( new Coordinate2D( x,y ), m_map );
                registerNonPlayerCharacter(lopezReggae);
            }
            
            @SuppressWarnings("unchecked")
            Iterator<Element> lopezReggaesAlados = npcData.elementIterator("business_logic.game_elements.LopezReggaeAlado");
            while( lopezReggaesAlados.hasNext() )
            {
                Element elem = lopezReggaesAlados.next();
                
                double x = Double.parseDouble(elem.attributeValue("x"));
                double y = Double.parseDouble(elem.attributeValue("y"));
                
                LopezReggaeAlado lopezReggaeAlado = new LopezReggaeAlado( new Coordinate2D( x,y ) );
                registerNonPlayerCharacter(lopezReggaeAlado);
            }
            
            Element wallData = root.element("Wall");
            @SuppressWarnings("unchecked")
            Iterator<Element> bloquesLadrillos = wallData.elementIterator("business_logic.game_elements.BloqueLadrillos");
            while( bloquesLadrillos.hasNext() )
            {
                Element elem = bloquesLadrillos.next();
                
                double x = Double.parseDouble(elem.attributeValue("x"));
                double y = Double.parseDouble(elem.attributeValue("y"));
                
                BloqueLadrillos bloqueLadrillos = new BloqueLadrillos( new Coordinate2D( x,y ) );
                registerWall(bloqueLadrillos,true);
            }
            
            @SuppressWarnings("unchecked")
            Iterator<Element> bloquesCemento = wallData.elementIterator("business_logic.game_elements.BloqueCemento");
            while( bloquesCemento.hasNext() )
            {
                Element elem = bloquesCemento.next();
                
                double x = Double.parseDouble(elem.attributeValue("x"));
                double y = Double.parseDouble(elem.attributeValue("y"));
                
                BloqueCemento bloqueCemento = new BloqueCemento( new Coordinate2D( x,y ) );
                registerWall(bloqueCemento,true);
            }
            
            @SuppressWarnings("unchecked")
            Iterator<Element> bloquesAcero = wallData.elementIterator("business_logic.game_elements.BloqueAcero");
            while( bloquesAcero.hasNext() )
            {
                Element elem = bloquesAcero.next();
                
                double x = Double.parseDouble(elem.attributeValue("x"));
                double y = Double.parseDouble(elem.attributeValue("y"));
                
                BloqueAcero bloqueAcero = new BloqueAcero( new Coordinate2D( x,y ) );
                registerWall(bloqueAcero,false);
            }
            
            Element dropData = root.element("Drop");
            @SuppressWarnings("unchecked")
            Iterator<Element> drops = dropData.elementIterator();
            while( drops.hasNext() )
            {
                Element elem = drops.next();
                
                // Class<? extends GameElement> elemClass = (Class<? extends GameElement>) Class.forName(elem.getName());
                @SuppressWarnings("unchecked")
                Class<? extends Element2D> elemClass = (Class<? extends Element2D>) Class.forName(elem.getName());
                Element2D dropInstance = elemClass.getDeclaredConstructor(Coordinate2D.class).newInstance( new Coordinate2D() );
                
                registerDrop(dropInstance);
            }
            
            /**
             * Configuro el tiempo del nivel
             */
            Element timeData = root.element("Score");
            m_timeLeft = Integer.parseInt(timeData.attributeValue("max"));
            m_winBonus = Integer.parseInt(timeData.attributeValue("win_bonus"));

            this.afterLoadLevel();
            
        }
        catch ( Exception ex )
        {
            ex.printStackTrace();
        }
    }
    
    /**
     * Métodos para agregar nuevos elementos al juego
     */
    
    private void addBasicElement( Element2D elem )
    {
        try
        {
            m_map.addElement(elem);
        }
        catch (ModelExceptions e)
        {
            e.printStackTrace();
        }
    }
    
    private void registerPlayerCharacter( Element2D elem )
    {
        m_player = (Bombita) elem;
        addBasicElement(elem);
    }
    
    private void registerNonPlayerCharacter( Element2D elem )
    {
        m_enemies.add((NonPlayerCharacter) elem);
        addBasicElement(elem);
    }
    
    private void registerDrop( Element2D elem )
    {
        m_drops.add( (GameElement) elem);
    }
    
    // TODO: aprolijar como se detecta si la pared puede dropear o no.
    private void registerWall( Element2D elem, boolean isDropper )
    {
        if( isDropper )
            m_droppers.add((GameElement)elem);
        
        addBasicElement(elem);
    }
    
    // TODO: revisar esto
    private void registerPortal(Portal portal)
    {
        // Lo registro como portal y lo añado primero a la lista de drops (asi me aseguro que va a ser el 1ero en ser asignado)
        m_portal = portal;
        m_drops.add(0, portal);
    }
    
    /**
     * Inicialización de niveles
     */
    
    private void randomlyAssignDrops()
    {
        // Hago una copia de la lista de drops, para evitar problemas de concurrencia
        LinkedList<GameElement> dropsCopy = new LinkedList<GameElement>(m_drops);
        
        // Mezclo los droppers
        Collections.shuffle(m_droppers);
        
        for ( GameElement drop : dropsCopy )
        {
            if ( !m_droppers.isEmpty() )
            {
                // Tomo el primer dropper (elegido al azar, ya que la lista esta mezclada)
                GameElement choosedDropper =  m_droppers.getFirst();
                
                // Le asigno el drop
                this.assignDrop( drop, choosedDropper );
            }
            
        }
    }
    
    // Despues se puden hacer estrategias de asignación de drop, por ahora es random
    private void assignDrop( GameElement drop, GameElement dropper )
    {
        // Asigno el drop al dropper
        ((Dropper)dropper).setDrop(drop);
        
        // Elimino el drop de la lista de drops sin asignar
        m_drops.remove(drop);
    
        // Elimino el dropper de la lista de droppers disponibles
        m_droppers.remove(dropper);
    }
    
    /**
     *  Estos métodos sirven para conocer el estado de la partida (ganada, perdida, etc.)
     */
    
    private boolean noMoreEnemiesAlive()
    {
        if(m_enemies.size() == 0)return true;
        
        int deadEnemiesCount = 0;
        for( NonPlayerCharacter enemy : m_enemies ) 
        {
            if( !enemy.isAlive() )
                deadEnemiesCount++;
            
            if( deadEnemiesCount == m_enemies.size() )
            {
                return true;
            }
        }
        return false;
    }
    
    private boolean playerIsDead()
    {       
        if ( m_player == null )
            return false;
        
        return !( m_player.isAlive() );
    }
    
    private void gameLost()
    {
        System.out.print("You died!\n");
        
        m_currentScore += m_timeLeft/2;
        m_gameLoop.add( new YouLoseText() );
        this.gameEnds();
    }
    
    private void levelWon()
    {
        System.out.print("Level Won!\n");
        m_currentScore += m_timeLeft + m_winBonus;
        m_forcedWinLevel = false;
        
        int currentLevelIndex = m_levels.indexOf( m_currentLevel );
        
        if ( m_levels.indexOf( m_levels.getLast() ) > currentLevelIndex )
        {
            m_currentLevel = m_levels.get(currentLevelIndex+1);
            this.loadPreMadeLevel( m_currentLevel );
        }
        else
        {
            this.gameWon();
        }
    }
    
    // TODO: OJO! aca no hacer un stop(), limpiar la pantalla y mostrar un mensaje y tu puntaje!
    // Tipo: Felicitaciones!!! Ganaste el juego! Tu puntaje es: (puntaje)
    // OTRA POSIBILIDAD ES: hacer el stop, pero antes activar una vista de gameWon
    private void gameWon()
    {
        System.out.println("Game Won!");
        System.out.println("Final Score: "+m_currentScore);
        m_gameLoop.add( new YouWinText() );
        this.gameEnds();
    }
    
    // Acá se pueden actualizar los scores
    private void gameEnds()
    {
        m_currentLevel = null;
        m_scoreMessage.updateScore(m_currentScore);
        m_highScores.addScore(m_currentScore);
        this.showScoreBoard();
        m_highScores.save(HIGHSCORES_FILENAME);
        
        this.stop();
    }
    
    private boolean portalHasBeenCrossed()
    {
        if ( m_portal == null )
            return false;
        
        return ( m_portal.getPlayerWon() && m_portal.getPosition().equals(m_player.getPosition()));
    }
    
    public void PlayerWinning(boolean bombitaWinning ) {
    	m_player.setBombitaWinning( bombitaWinning );
    }
}
